# 
# Initial blush at system related euca events
#
# Cloud Controller :: queries (Instance managers) for information about 
#   resources, makes high level scheduling decisions, and implements them 
#   by making requests to group managers.
#
# Cluster Controller :: Gathers information about and schedules VM execution 
#   on specific instance managers, as well as manage virtual instance network.
#
# Node Controller :: Controls the execution, inspection and termination of 
#   VM instances on the host that it runs.
#

module EUCA;

export {

	global euca_log = open_log_file("euca");

	redef enum Notice += {
		EUCA_OpenPortMax,
		EUCA_ProhibSvc,
	};

# reservations hold instances.  Since firewall rules are bound to groups
#   the reservation object holds them.
#

type instObj: record {
	#pub_addr: set[string];	# this is a string set since we may get 
	pub_addr: string;	# this is a string set since we may get 
				#   a domain name rather than an IP
	#priv_addr: set[string];
	priv_addr: string;
	init_time: time;
	state: string;
};

type resObj: record {
	instSet: table[string] of instObj;
	group: string;
	key: string;
	zone: string;
	r_id: count;
};

type groupObj: record {
	firewall_rules: set[string];
	open_tport_count: count &default = 0;
	open_uport_count: count &default = 0;
};

# every IP address has these characteristics.  If any of them
#  change this is an interesting and noteworthy thing...
type addrObj: record {
	reservation: string;
	instance: string;
	owner: string;
	group: string;
};
	
# This table holds the euca instance data designed to be interacted with
#  via the euca_* events.  For connection data we need to have a much faster
#  lookup table with instance data in it.
# Since the owner id is the most consistant data across provided info, we use
#  it for the main index here.
type eucaIndexObj: record {
	res: table[string] of resObj;	# holds instance objs
	grp: table[string] of groupObj;	# holds firewall rules which apply to res
	u_id: count;			# unique id for user.
};

type badSvc: record {
	protocol: string;
	prt: count;
};

# main container for "stuff"
global euca_index: table[string] of eucaIndexObj;

# said faster table for IP lookups
global ip_map: table[addr] of string;
# bookeeping table for instance -> reservation mapping
global instance_map: table[string] of string;


# session ID
global cluster_sess_id = 0;

# firewall configs
global firewall_banned_svcs: set[badSvc];
global tport_threshold = 64000;
global uport_threshold = 64000;

# image data
global vetted_images: set[string];

}

#
# function list

function register_ip_res(ip: string, res: string) : count 
{
	local ret_val: count = 0;
	local ip_t: addr = to_addr(ip);

	if ( ip_t !in ip_map ) {

		ip_map[ip_t] = res;
		ret_val = 1;
		}

	return ret_val;
}

function register_inst_res(inst: string, res: string) : count
{
	local ret_val: count = 0;

	if ( inst !in instance_map ) {
		
		instance_map[inst] = res;
		ret_val = 1;
		}

	return ret_val;
}

function lookup_ip_res(ip: string) : string
{
	local ret_val = "UNKNOWN";
	local ip_t: addr = to_addr(ip);

	if ( ip_t in ip_map )
		ret_val = ip_map[ip_t];

	return ret_val;
}

function lookup_inst_res(inst: string) : string
{
	local ret_val = "UNKNOWN";

	if ( inst in instance_map )
		ret_val = instance_map[inst];

	return ret_val;
}

function test_user(res_owner: string) : eucaIndexObj
{
	local t_eucaIndexObj: eucaIndexObj;

	if ( res_owner !in euca_index ) {

		local t_resObj: resObj;
		local t_groupObj: groupObj;
	
		#t_eucaIndexObj$res = t_resObj;
		#t_eucaIndexObj$grp = t_groupObj;
		t_eucaIndexObj$u_id = ++cluster_sess_id;

		euca_index[res_owner] = t_eucaIndexObj;

		print euca_log, fmt("%.6f #u%s AddUser %s", network_time(), t_eucaIndexObj$u_id, res_owner);
		
		}
	else {
		t_eucaIndexObj = euca_index[res_owner];
		}

	return t_eucaIndexObj;
}

#
function test_reservation(res_id: string, res_owner: string, res_group: string, key_name: string, avail_zone: string) : resObj
{
	# if res_id exists in reserve_list, just return it, else
	#  create it
	
	local t_resObj: resObj;
	local t_eucaIndexObj: eucaIndexObj;

	t_eucaIndexObj = test_user(res_owner);

	if ( res_id !in t_eucaIndexObj$res ) {

		# Obj is not in the table, so we fill in what we can.
		# Remaining structs will be filled in later.
		t_resObj$group = res_group;
		t_resObj$key   = key_name;
		t_resObj$zone  = avail_zone;
		t_resObj$r_id  = ++cluster_sess_id;

		t_eucaIndexObj$res[res_id] = t_resObj;

		print euca_log, fmt("%.6f #u%s #r%s AddReservation %s %s @ %s key: %s zone: %s",
			network_time(), t_eucaIndexObj$u_id, cluster_sess_id, res_id, res_owner, res_group, 
			key_name, avail_zone);

		}
	else {
		t_resObj = t_eucaIndexObj$res[res_id];
		}

	return t_resObj;
}

# Not all of the variables here are necessisary to generate an instance object. Why
#  we create such a mess is that all the parent objects might have to be created as
#  well, so we one stop shop the mess...
function test_instance(res_id: string, res_owner: string, res_group: string, key_name: string, avail_zone: string, inst_id: string, inst_img_id: string, priv_dns: string, pub_dns: string, state: string, inst_indx: count, start_time: time) : instObj
{
	local t_instObj: instObj;
	local t_resObj: resObj;	
	local t_eucaIndexObj: eucaIndexObj;

	t_eucaIndexObj = test_user(res_owner);
	t_resObj = test_reservation(res_id, res_owner, res_group, key_name, avail_zone);

	if ( inst_id !in t_resObj$instSet ) {

		#local t_pub_addr: set[string];
		#local t_priv_addr: set[string];

		#add t_pub_addr[pub_dns];
		#add t_priv_addr[priv_dns];

		t_instObj$pub_addr = pub_dns;
		t_instObj$priv_addr = priv_dns;
		t_instObj$init_time = start_time;
		t_instObj$state = state;

		t_resObj$instSet[inst_id] = t_instObj;
		euca_index[res_owner]$res[res_id] = t_resObj;

		print euca_log, fmt("%.6f #u%s #r%s AddInstance %s %s:>%s %s -> %s", 
			network_time(), t_eucaIndexObj$u_id, t_resObj$r_id,
			res_id, inst_id, priv_dns, pub_dns, inst_indx);

		}
	else {
		# just scoop up the value as it is in the table
		#  and test for change in state
		t_instObj = t_resObj$instSet[inst_id];

		if (t_instObj$state != state) {

			print euca_log, fmt("%.6f #u%s #r%s StateChange %s::%s %s -> %s", 
				network_time(), t_eucaIndexObj$u_id, t_resObj$r_id, 
				res_id, inst_id, t_instObj$state, state);

			t_instObj$state = state;

			t_resObj$instSet[inst_id] = t_instObj;
			euca_index[res_owner]$res[res_id] = t_resObj;

			}
		}

	return t_instObj;
}

# Process firewalls
# euca_firewall yin	default	ALLOWS	tcp	22	22	0.0.0.0/0
# id, group, type, protocol, start-p, end-p, from cidr
#
function process_firewall_rule(res_owner: string, res_group: string, prot: string, start_p: count, end_p: count, from_net: string) : count
{
	# A group's security policy is defined by the set of firewall policies
	#
	local ret_val: count = 0;
	local blank_subnet = "None";

	local t_eucaIndexObj = test_user(res_owner);
	local t_groupObj: groupObj; 

	# For the time being we are only interested in 'ALLOW' rules
	#  so rules what are 'DENY' or involve a CIDR of "None" in
	#  the source field will get ignored.
	if ( from_net == blank_subnet ) {
		ret_val = 1;
		}
	else {
		# Something to chew on...
		# The 'from' network will be used as an index value into the 
		#  set of firewall rules applied to it
		#
		# As well we need to deal with seeing these rules more than one time.  This
		#  is not a bulletproof (or even resistant solution)...

		local rule: string = fmt("%s-%s-%s-%s-%s", res_group, prot, start_p, end_p, from_net);

		if ( from_net !in t_eucaIndexObj$grp ) {
			
			local t_firewall_rules: set[string];
			add t_firewall_rules[rule];

			t_groupObj$open_tport_count = 0;	
			t_groupObj$open_uport_count = 0;	
			t_groupObj$firewall_rules = t_firewall_rules;
			# now just plug it in
			t_eucaIndexObj$grp[from_net] = t_groupObj;
			}
		else {
			t_groupObj = t_eucaIndexObj$grp[from_net];
			}

		if ( rule in t_eucaIndexObj$grp[from_net]$firewall_rules ) {
			# been here before ...
			return ret_val;
			}
		
		# new rule - add it!
		add t_eucaIndexObj$grp[from_net]$firewall_rules[rule];
		
		# Count the number of open ports to make sure that nothing
		#  too stupid is going on.
		local port_range: count = end_p - start_p;

		if ( port_range == 0 )
			port_range = 1;

		if ( prot == "tcp" ) {

			if ( (t_groupObj$open_tport_count =+ port_range) >= tport_threshold ) {
				NOTICE([$note=EUCA_OpenPortMax,
					$msg=fmt("%n tcp ports open, owner %s group %s ",
						t_groupObj$open_tport_count, res_owner, res_group)]);
				}
			}
		else if ( prot == "udp" ) {

			if ( (t_groupObj$open_uport_count =+ port_range) >= uport_threshold ) {
				NOTICE([$note=EUCA_OpenPortMax,
					$msg=fmt("%n udp ports open, owner %s group %s ",
						t_groupObj$open_uport_count, res_owner, res_group)]);
				}

			} # end udp

		# Having made a guess as to the number of ports being opened
		#  take another gander at *what* is being opened.  Anything bad?
		# There is a list of bad things which is of the form set[prot,port].
		# The port type is not used since most of the data we get is a
		#  range.  This is a vile and inefficient way to do this.
		local t_badSvc: badSvc;

		for ( t_badSvc in firewall_banned_svcs ) { 
			
			if ( t_badSvc$protocol == prot ) {
				if ( (t_badSvc$prt >= start_p) && (t_badSvc$prt <= end_p) ) {
					NOTICE([$note=EUCA_ProhibSvc,
						$msg=fmt("service %s-%s / %s %s@%s",
							t_badSvc$prt, t_badSvc$protocol, t_groupObj$open_uport_count, 
							res_owner, res_group)]);

					} # end port match
				} # end protocol match
		
			} # end bad svc
		} # end blank line

	return ret_val;
}
# #################################################### #
# event list
#

#  euca-describe-addresses:
#  ADDRESS 10.193.175.20 i-00000008 (uid) => make events {taddress, instance id}
#event euca_address(inst_ip: addr, inst_id: string, user: string)
#{
	
#}

# bro-describe-instances: sample output
# r-3B3306F8	scottc	default	i-41A908F0	emi-B2560FA7	128.55.70.201	192.168.3.70	
#	running 	userkey 	0 	c1.medium 	1310423835.61
#	euca 	eki-90B50F4F 	eri-A3940FC2
# r-3CF706C6	scottc	default	i-34B3073C	emi-B2560FA7	128.55.70.215	192.168.3.66	
#	running 	userkey 	0 	c1.medium 	1310423835.61
#	euca 	eki-90B50F4F 	eri-A3940FC2
# r-3CF706C6	scottc	default	i-37C60781	emi-B2560FA7	128.55.70.218	192.168.3.68
#	running 	userkey 	2 	c1.medium 	1310423835.61
#	euca 	eki-90B50F4F 	eri-A3940FC2
# r-3CF706C6	scottc	default	i-44E807C8	emi-B2560FA7	128.55.70.217	192.168.3.67	
#	running 	userkey 	1 	c1.medium 	1310423835.61
#	euca 	eki-90B50F4F 	eri-A3940FC2
# r-3CF706C6	scottc	default	i-4AD8082B	emi-B2560FA7	128.55.70.219	192.168.3.69	
#	running 	userkey 	3 	c1.medium 	1310423835.61
#	euca 	eki-90B50F4F 	eri-A3940FC2
#
# struct: RES_ID, RES_OWNER_ID, RES_GROUP_ID
#	  INST_ID, INST_IMAGE_ID, INST_PUB_DNS_NAME, INST_PRIV_DNS_NAME
#	  INST_STATE { pending|running|shutting-down }
#	  INST_KEY_NAME, INST_INDEX, INST_TYPE, INST_L_TIME, AVAIL_ZONE
#	  INST_KERNEL, INST_RAM_DISK
#
# Base event for filling in running instances.  Each reservation/instance pair will
#  generate one of these events.  Implementation is simple rather than efficient
#  but will loop over all pairs.
#

event euca_instances(res_id: string, res_owner: string, res_group: string, inst_id: string, inst_img_id: string, priv_dns: string, pub_dns: string, state: string, key_name: string, inst_indx: count, start_time: time, avail_zone: string)
{
	local t_instObj: instObj;
	local t_resObj: resObj;

	# Do a quick test to see if the reservation exists.  Fill in the blanks.
	if ( res_id !in euca_index ) {
		t_resObj = test_reservation(res_id, res_owner, res_group, key_name, avail_zone);
		}

	# the instance needs to be tested every time since the state may change
	t_instObj = test_instance(res_id, res_owner, res_group, key_name, avail_zone, inst_id, inst_img_id, priv_dns, pub_dns, state, inst_indx, start_time);

}
 


